package com.hzit.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.AggregateBuilders;
import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.util.ObjectBuilder;
import com.alibaba.fastjson.JSON;
import com.hzit.search.service.ItemSearchService;
import com.hzit.search.vo.ItemVo;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.system.api.domain.Item;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.client.elc.*;
import org.springframework.data.elasticsearch.core.AggregationsContainer;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.redis.connection.SortParameters;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 类名：
 * 作者：WF
 * 功能：
 */
@Service
public class ItemSearchServiceImpl implements ItemSearchService {
	@Autowired
	private ElasticsearchClient client;
	@Autowired
	private ElasticsearchRestTemplate template;
	@Autowired
	private StringRedisTemplate redisTemplate;

	//1. 进行索引库查询（版本一：使用RestClient进行查询：目前流行做法）
	@Override
	public Map<String, Object> search(ItemVo vo) throws IOException {
		Map<String, Object> resultMap = new HashMap<>();
		//1.1 定义查询到的数据集合
		List<Item> itemList = new ArrayList<>();
		//1.2 定义查询对象
		SearchRequest request = null;
		//1.3 定义所有的过滤查询集合对象
		List<Query> filterQuery = new ArrayList<>();
		//1.4 定义排序列表
		List<SortOptions> sortOptions = new ArrayList<>();
		//1.5 定义分类集合(存放聚合后的分类)
		List<String> categories = new ArrayList<>();
		//1.6 得到分页参数的值
		int page = vo.getPage();            // 当前页
		int pageSize = vo.getPageSize();    // 每页大小
		//1.7 判断查询关键字是否有值，如果有值就进行高亮查询及其它查询
		if(StringUtils.isNotBlank(vo.getKeywords())){
			//1.8 定义多字段查询
			//1.8.1 定义多字段匹配查询对象
			Query mmQuery = MultiMatchQuery.of(mq -> mq
					.fields("title", "brand", "category")     // 索引库的字段名
					.query(vo.getKeywords())                            // 字段的值
			)._toQuery();
			//1.8.2 将多字段匹配查询对象放到过滤查询集合中
			filterQuery.add(mmQuery);
			//1.9 根据分类进行查询
			if(StringUtils.isNotBlank(vo.getCategory())){
				//1.9.1 构造一个分类的查询条件
				Query categoryQuery = TermQuery.of(tq -> tq.field("category").value(vo.getCategory()))._toQuery();
				//1.9.2 将其放到查询集合中
				filterQuery.add(categoryQuery);
			}
			//1.10 进行品牌查询
			if(StringUtils.isNotBlank(vo.getBrand())){
				//1.10.1 构造一个品牌查询的对象
				Query brandQuery = TermQuery.of(tq -> tq.field("brand").value(vo.getBrand()))._toQuery();
				//1.10.2 将其放入到查询集合中
				filterQuery.add(brandQuery);
			}
			//1.11 进行规格查询
			if(vo.getSpec() != null && vo.getSpec().size() > 0){
				//1.11.1 对规格进行拆分，
				List<String> spec = vo.getSpec();
				for (String s : spec) {
					//其中：split[0]: 规格的名字 split[1]: 规格选项的值
					String[] split = s.split(":");
					//1.11.2 构造规格查询对象
					Query specQuery = TermQuery.of(tq -> tq.field("specMap." + split[0] + ".keyword").value(split[1]))._toQuery();
					//1.11.3 将规格查询对象放到查询列表中
					filterQuery.add(specQuery);
				}
			}
			//1.12 进行价格区间查询
			if(StringUtils.isNotBlank(vo.getPrice())){
				//1.12.1 进行拆分
				String price = vo.getPrice();
				String[] split = price.split("-");      // split[0]: 最小值 split[1]: 最大值
				//1.12.2 构造查询对象
				Query priceQuery = null;
				if(split[1].equals("*")){
					priceQuery = RangeQuery.of(rq -> rq.field("price").gte(JsonData.of(split[0])))._toQuery(); // gte: 代表greate than eaual(  大于等于 )
				}else{
					priceQuery = RangeQuery.of(rq -> rq.field("price").gte(JsonData.of(split[0])).lte(JsonData.of(split[1])))._toQuery(); // lte: 小于等于(less than eauals)
				}
				filterQuery.add(priceQuery);
			}
			//1.13 进行排序查询
			if(StringUtils.isNotBlank(vo.getSort())){
				//1.13.1 对排序字段进行拆分
				String sort = vo.getSort();
				String[] split = sort.split(":");       // split[0]: 排序字段 split[1]:  排序规则 （asc：升序 desc：降序）
				SortOptions options = null;
				if(split[1].equals("asc")){
					//定义排序操作
					options = SortOptions.of(so -> so.field(sf -> sf.field(split[0]).order(co.elastic.clients.elasticsearch._types.SortOrder.Asc)));

				}else{
					options = SortOptions.of(so -> so.field(sf -> sf.field(split[0]).order(co.elastic.clients.elasticsearch._types.SortOrder.Desc)));
				}
				//1.13.2 将其放到排序集合中
				sortOptions.add(options);
			}
			// 1.14 准备好条件后，开始查询
			request = SearchRequest.of(
					sr -> sr.index("item")
							//1.14.1 定义各种过滤查询
							.query(q -> q.bool(bq -> bq.filter(filterQuery))
							)
							//1.14.2 高亮查询
							.highlight(hl -> hl
									.preTags("<span style='color:red'>")                // 前缀字段
									.postTags("</span>")                                // 后缀字段
									.fields("title",hlf -> hlf))  // 参数1：高亮查询字段名 参数2：高亮查询的值
							//1.14.3 指定聚合的字段
							.aggregations("categoryAgg",cateAgg -> cateAgg.terms(tq -> tq.field("category")))
							//1.14.4 定义排序
							.sort(sortOptions)
							//1.14.5 分页查询
							.from((page-1)*pageSize)        // 分页开始
							.size(pageSize)                 // 每页显示记录数
			);
		}else{              // 没有指定关键字时进行的查询
			request = SearchRequest.of(sr -> sr
					.index("item")
					//分页查询
					.from((page-1)*pageSize)        // 分页开始
					.size(pageSize)                 // 每页显示记录数
					.query(q -> q.matchAll(ma -> ma)));

		}
		// 1.15 开始查询
		SearchResponse<Item> search = client.search(request, Item.class);
		// 1.16 得到查询结果
		HitsMetadata<Item> hits = search.hits();
		// 1.17 得到总记录数
		long value = hits.total().value();
		// 1.18 计算总页数
		int totalPage = (int) Math.ceil(value / (double)pageSize);
		//1.19 处理查询结果数据
		for (Hit<Item> hit : hits.hits()) {
			//1.19.1 得到高亮数据（key：高亮字段，value：高亮数据）
			Map<String, List<String>> highlight = hit.highlight();
			//1.19.2 取得高亮数据
			List<String> list = highlight.get("title");
			//1.19.3 如果有值，就取出值，得到其第一个就是title的值
			Item item = hit.source();       // 原始数据,如：item.title = "这是华为手机“
			if(!Objects.isNull(list) && list.size() > 0){
				String title = list.get(0);     // 经过高亮处理后的title数据,值就是：这是<span style='color:red'>华为</span>手机
				item.setTitle(title);
			}
			//1.19.4 将结果放到集合中
			itemList.add(item);                 // 将此item放到高亮数据集合中
		}
		//1.20 处理分类数据
		if(StringUtils.isNotBlank(vo.getKeywords())){
			//1.20.1 得到聚合的数据
			Map<String, Aggregate> aggregations = search.aggregations();
			//1.20.2 根据聚合的key得到指定的聚合数据
			Aggregate categoryAgg = aggregations.get("categoryAgg");
			List<StringTermsBucket> termsBuckets = categoryAgg.sterms().buckets().array();
			//1.20.3 遍历聚合数据并放到分类集合中
			for (StringTermsBucket termsBucket : termsBuckets) {
				String key = termsBucket.key();
				categories.add(key);
			}
		}
		// 1.21.1 根据分类在redis中查询品牌及规格列表
		String category = null;
		if(categories.size() > 0){
			category = categories.get(0);
		}
		if(StringUtils.isNotBlank(vo.getCategory())){
			category = vo.getCategory();
		}
		//如果上面查询出来的category仍然没有值，证明的确前端根本没有分类传入或后端分类列表中也没有值，
		//只有一种情况，就是分类中没有值，此时品牌及规格列表也应该没有数据
		//1. 从redis中根据分类查询品牌及规格列表
		Map brandAndSpecMap = findSpecAndBrandByCategory(category);

		//最后，放到resultMap集合中
		resultMap.put("itemList",itemList);
		resultMap.put("categories",categories);
		resultMap.putAll(brandAndSpecMap);
		resultMap.put("total",value);               // 总记录数
		resultMap.put("totalPage",totalPage);       // 总页数

		// 这是<span style='color:red'>华为</span>手机
		return resultMap;
	}

	//2. 进行索引库查询（版本二：使用RestHighLevleClient进行查询：原来的做法）：【了解】
	public Map<String,Object> search2(ItemVo vo){
		Map<String,Object> resultMap = new HashMap<>();
		List<Item> itemList = new ArrayList<>();
		//2.1 构造一个NativeSearchQueryBuilder
		NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();
		if(StringUtils.isNotBlank(vo.getKeywords())){
			//2.2 构造多字段匹配查询
			searchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(vo.getKeywords(),"title","brand","category"));
			//2.3 聚合查询出分类数据
			searchQueryBuilder.withAggregations(AggregationBuilders.terms("categoryGroup").field("category"));
			//2.4 进行过滤查询
			//2.4.1 定义组合查询对象
			BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
			//2.4.2 定义高亮查询
			searchQueryBuilder.withHighlightBuilder(new HighlightBuilder().field("title").preTags("<span style='color:red'>").postTags("</span>"));
			//2.4.3 进行分类查询
			if(StringUtils.isNotBlank(vo.getCategory())){
				boolQueryBuilder.filter(QueryBuilders.termQuery("category",vo.getCategory()));
			}
			//2.4.4 进行品牌查询
			if(StringUtils.isNotBlank(vo.getBrand())){
				boolQueryBuilder.filter(QueryBuilders.termQuery("brand",vo.getBrand()));
			}
			//2.4.5 进行规格查询
			if(vo.getSpec() != null && vo.getSpec().size() > 0){
				for (String s : vo.getSpec()) {
					String[] split = s.split(":");      // split[0]: 规格名字 split[1]: 规格值
					boolQueryBuilder.filter(QueryBuilders.termQuery("specMap." + split[0] + ".keyword",split[1]));
				}
			}
			//2.4.6  价格区间查询
			if(StringUtils.isNotBlank(vo.getPrice())){
				String[] split = vo.getPrice().split(":");
				if(split[1].equals("*")){
					boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]));
				}else{
					boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]).lte(split[1]));
				}
			}
			//2.5 排序查询
			if(StringUtils.isNotBlank(vo.getSort())){
				String sort = vo.getSort();
				String[] split = sort.split(":");   // split[0]: 排序字段，split[1]: 排序规则
				searchQueryBuilder.withSort(SortBuilders.fieldSort(split[0]).order(split[1].equals("asc")? SortOrder.ASC:SortOrder.DESC));
			}
			//2.6 分页查询
			searchQueryBuilder.withPageable(PageRequest.of(vo.getPage()-1,vo.getPageSize()));

			//2.7 将布尔查询（组合查询）与NativeSearchQueryBuilder进行绑定
			searchQueryBuilder.withFilter(boolQueryBuilder);
		}else{
			searchQueryBuilder.withQuery(QueryBuilders.matchAllQuery()).withPageable(PageRequest.of(vo.getPage()-1,vo.getPageSize()));
		}

		//2.8 开始条件查询
		SearchHits<Item> search = template.search(searchQueryBuilder.build(), Item.class, IndexCoordinates.of("item"));
		long total = search.getTotalHits();     // 得到总记录数
		//2.9 计算总页数
		int totalPage = (int) Math.ceil(total/(double)vo.getPageSize());
		//2.10 处理查询结果
		List<SearchHit<Item>> searchHits = search.getSearchHits();
		for (SearchHit<Item> searchHit : searchHits) {
			Item content = searchHit.getContent();
			//2.11 处理高亮字段的值
			List<String> hitHighlightField = searchHit.getHighlightField("title");
			if(hitHighlightField != null && hitHighlightField.size() > 0){
				String title = hitHighlightField.get(0);
				content.setTitle(title);
			}
			itemList.add(content);
		}
		//2.12 处理分类聚合后的数据（分类数据）
		AggregationsContainer<?> aggregations = search.getAggregations();
		List<String> categoryList = null;
		if(aggregations != null) {
			Aggregations aggs = (Aggregations) aggregations.aggregations();
			ParsedStringTerms categoryGroup = aggs.get("categoryGroup");
			categoryList = categoryGroup.getBuckets().stream().map(m -> m.getKeyAsString()).collect(Collectors.toList());
		}
		// 2.13 根据分类在redis中查询品牌与规格
		String category = null;
		if(categoryList != null && categoryList.size() > 0){
			category = categoryList.get(0);
		}
		if(StringUtils.isNotBlank(vo.getCategory())){
			category = vo.getCategory();
		}
		//如果上面查询出来的category仍然没有值，证明的确前端根本没有分类传入或后端分类列表中也没有值，
		//只有一种情况，就是分类中没有值，此时品牌及规格列表也应该没有数据
		//1. 从redis中根据分类查询品牌及规格列表
		Map brandAndSpecMap = findSpecAndBrandByCategory(category);
		resultMap.put("itemList",itemList);
		resultMap.put("categories",categoryList);
		resultMap.putAll(brandAndSpecMap);
		resultMap.put("totalPage",totalPage);
		resultMap.put("total",total);
		return resultMap;
	}

	//3. 在redis中根据分类名字找到品牌及规格列表
	private Map findSpecAndBrandByCategory(String category) {
		Map map = new HashMap();
		//1. 没有分类时，向返回结果中放空集合
		if(category == null){
			map.put("brandList",new ArrayList<>());
			map.put("specList",new ArrayList<>());
		}else{
			//2. 根据分类名得到模板id
			String typeId = (String) redisTemplate.opsForHash().get("itemCats", category);
			//3. 根据模板id得到品牌列表
			String brands = (String) redisTemplate.opsForHash().get("brandList", typeId);
			List<Map> brandList = JSON.parseArray(brands, Map.class);
			//4. 放到map中
			map.put("brandList",brandList);

			//5. 根据模板id查询出规格及规格选项列表
			String specs = (String) redisTemplate.opsForHash().get("specList", typeId);
			List<Map> specList = JSON.parseArray(specs, Map.class);
			//6. 放到map中
			map.put("specList",specList);
		}
		//7. 最后返回map
		return map;
	}
}
